/*
 * File     : NormalStructureSubBuilder.java
 * Created  : 1 May 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.structure;

import static com.googlecode.dni.internal.DniInternal.LOGGER;
import static com.googlecode.dni.internal.DniTypes.*;
import static org.objectweb.asm.Opcodes.*;

import java.lang.reflect.Method;
import java.util.Comparator;
import java.util.List;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;

import com.googlecode.dni.internal.DniInternal;
import com.googlecode.dni.internal.MemoryAccess;
import com.googlecode.dni.internal.structure.StructureBuilder.StructureSubBuilder;
import com.googlecode.dni.type.structure.Padding;
import com.googlecode.dni.type.structure.Structure;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
 * <p>
 *  Generates byte-code for implementations of structure types, where the
 *  structure is not opaque.
 * </p>
 *
 * @author Matthew Wilson
 */
@SuppressWarnings( "nls" )
public final class NormalStructureSubBuilder implements StructureSubBuilder
{

    private final StructureBuilder< ? > parent;

    /** The largest structure field unit size; used for padding calculations. */
    private int largestFieldUnitSize;

    private StructureFieldFactory structureFieldFactory;


    /**
     * @param parent
     *            the parent
     */
    NormalStructureSubBuilder( final StructureBuilder< ? > parent )
    {
        this.parent = parent;
    }

    @Override
    public void writeMainClassMethods( final ClassWriter classWriter,
                                       final List< Method > methods )
    {
        if ( methods.isEmpty() )
        {
            throw new IllegalArgumentException( "Structure must define at least one field: " + this.parent.getType().getName() );
        }

        this.structureFieldFactory = new StructureFieldFactory( classWriter,
                                                                getImplName(),
                                                                this.parent.getFastCharset(),
                                                                this.parent.getSession() );

        // 1) find getters, defining fields
        for ( Method method : methods )
        {
            this.structureFieldFactory.addMethod( method );
        }

        // 2) find equivalent setters and 3) check all fields are complete
        this.structureFieldFactory.completedMethods();

        // 4) sort the fields by index
        this.structureFieldFactory.sortFields( new FieldComparator() );

        // 5) allocate offsets
        int calculatedSize = setOffsets( this.parent.getPadding() );
        if ( this.parent.getSize() < 0 )
        {
            this.parent.setSize( calculatedSize );
        }
        else if ( this.parent.getSize() < calculatedSize )
        {
            LOGGER.warning( "Fixed size is too small for " + this.parent.getType().getName()
                            + "; calculcated = " + calculatedSize + ", fixed = " + this.parent.getSize() );
        }

        // 6) create the class
        writeMainClassMethodsImpl();
    }

    /** {@inheritDoc} */
    @Override
    public void writeStaticInitialiserFragment( final MethodVisitor methodVisitor )
    {
        this.structureFieldFactory.writeStaticInitialiserFragment( methodVisitor );
    }

    @Override
    public String getStructureBaseType()
    {
        return ABSTRACT_STRUCTURE_TYPE;
    }

    @Override
    public String getFactoryBaseType()
    {
        return ABSTRACT_STRUCTURE_FACTORY_TYPE;
    }

    @Override
    public void writeAllocateUntypedFactoryMethodImpl( final MethodVisitor methodVisitor )
    {
        methodVisitor.visitTypeInsn( NEW, this.parent.getImplName() );
        methodVisitor.visitInsn( DUP );
        methodVisitor.visitMethodInsn( INVOKESPECIAL,
                                       this.parent.getImplName(),
                                       "<init>",
                                    "()V" );
        methodVisitor.visitInsn( ARETURN );
    }

    @Override
    public int getLargestFieldUnitSize()
    {
        return this.largestFieldUnitSize;
    }

    private Class< ? > getType()
    {
        return this.parent.getType();
    }

    private String getImplName()
    {
        return this.parent.getImplName();
    }

    private int setOffsets( final Padding padding )
    {
        int offset = 0;
        int calculatedSize = 0;
        this.largestFieldUnitSize = 0;
        for ( StructureField field : this.structureFieldFactory.getOrderedFields() )
        {
            int desiredOffset = field.getOffset();
            int fieldSize = field.getSize();
            int fieldPaddingUnitSize = field.getPaddingUnitSize();
            if ( desiredOffset < 0 )
            {
                offset = padding.pad( offset, fieldPaddingUnitSize );
                field.setOffset( offset );
            }
            else
            {
                offset = desiredOffset;
            }

            this.largestFieldUnitSize = Math.max( this.largestFieldUnitSize, fieldPaddingUnitSize );

            offset += fieldSize;
            calculatedSize = Math.max( offset, calculatedSize );
        }

        calculatedSize = padding.pad( calculatedSize, this.largestFieldUnitSize );

        return calculatedSize;
    }

    /**
     * Writes the class with fields and methods.
     */
    private void writeMainClassMethodsImpl()
    {
        ClassWriter classWriter = this.structureFieldFactory.getClassWriter();

        writeDefaultConstructor( classWriter );
        writeCopyConstructor( classWriter );
        writePointerConstructor( classWriter );

        this.structureFieldFactory.writeMethods();

        // Object methods
        writeEqualsMethod( classWriter );
        writeHashCodeMethod( classWriter );
        this.parent.writeToStringMethod();

        // Structure methods
        writeToDebugStringMethod( classWriter );
        writeCopyMethod( classWriter );
        writeCopyFromMethod( classWriter );
    }

    /**
     * Writes a default constructor.  This allocates the memory required.
     *
     * @param classWriter
     *            the class writer
     */
    private void writeDefaultConstructor( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC, "<init>", "()V", null, null );
        methodVisitor.visitCode();

        //-- super( MemoryAccess.ALLOCATOR.allocate( <size> ).getAddress() );
        methodVisitor.visitVarInsn( ALOAD, 0 );

        methodVisitor.visitFieldInsn( GETSTATIC,
                                      MEMORY_ACCESS_TYPE,
                                      "ALLOCATOR",
                                      "L" + ALLOCATOR_TYPE + ";" );
        methodVisitor.visitLdcInsn( (long) this.parent.getSize() );
        methodVisitor.visitMethodInsn( INVOKEINTERFACE,
                                       ALLOCATOR_TYPE,
                                       "allocate",
                                       "(J)L" + POINTER_TYPE + ";" );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       POINTER_TYPE,
                                       "address",
                                       "()J" );

        methodVisitor.visitMethodInsn( INVOKESPECIAL, ABSTRACT_STRUCTURE_TYPE, "<init>", "(J)V" );

        this.structureFieldFactory.writeConstructorFragment( methodVisitor );

        // return
        methodVisitor.visitInsn( RETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes a copy constructor.  This takes another instance, allocating
     * memory required.
     *
     * @param classWriter
     *            the class writer
     */
    private void writeCopyConstructor( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC,
                                                               "<init>",
                                                               "(L" + getImplName() + ";)V",
                                                               null,
                                                               null );
        methodVisitor.visitCode();

        //-- super( MemoryAccess.ALLOCATOR.copy( other.pointer(), <size> ).getAddress() );
        methodVisitor.visitVarInsn( ALOAD, 0 );

        methodVisitor.visitFieldInsn( GETSTATIC,
                                      MEMORY_ACCESS_TYPE,
                                      "ALLOCATOR",
                                      "L" + ALLOCATOR_TYPE + ";" );

        methodVisitor.visitVarInsn( ALOAD, 1 );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       ABSTRACT_STRUCTURE_TYPE,
                                       "pointer",
                                       "()L" + POINTER_TYPE + ";" );

        DniInternal.writeConstantInt( methodVisitor, this.parent.getSize() );
        methodVisitor.visitInsn( I2L );

        methodVisitor.visitMethodInsn( INVOKEINTERFACE,
                                       ALLOCATOR_TYPE,
                                       "copy",
                                       "(L" + POINTER_TYPE + ";J)L" + POINTER_TYPE + ";" );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       POINTER_TYPE,
                                       "address",
                                       "()J" );

        methodVisitor.visitMethodInsn( INVOKESPECIAL, ABSTRACT_STRUCTURE_TYPE, "<init>", "(J)V" );

        for ( StructureField field : this.structureFieldFactory.getOrderedFields() )
        {
            field.writeConstructorFragment( methodVisitor, getImplName() );
        }

        // return
        methodVisitor.visitInsn( RETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes a pointer constructor.  This takes existing memory and an owner.
     *
     * @param classWriter
     *            the class writer
     */
    private void writePointerConstructor( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC,
                                                               "<init>",
                                                               "(J)V",
                                                               null,
                                                               null );
        methodVisitor.visitCode();

        //-- super( pointer );
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitVarInsn( LLOAD, 1 );
        methodVisitor.visitMethodInsn( INVOKESPECIAL, ABSTRACT_STRUCTURE_TYPE, "<init>", "(J)V" );

        for ( StructureField field : this.structureFieldFactory.getOrderedFields() )
        {
            field.writeConstructorFragment( methodVisitor, getImplName() );
        }

        // return
        methodVisitor.visitInsn( RETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes the {@link Structure#equals(Object)} method
     *
     * @param classWriter
     *            the class writer
     */
    private void writeEqualsMethod( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC,
                                                               "equals",
                                                               "(Ljava/lang/Object;)Z",
                                                               null,
                                                               null );
        methodVisitor.visitCode();

        // if ( obj == null )
        //     return false;
        methodVisitor.visitVarInsn( ALOAD, 1 );
        Label continueLabel1 = new Label();
        methodVisitor.visitJumpInsn( IFNONNULL, continueLabel1 );
        DniInternal.writeConstantInt( methodVisitor, 0 );
        methodVisitor.visitInsn( IRETURN );
        methodVisitor.visitLabel( continueLabel1 );

        // if ( this == obj )
        //     return true;
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitVarInsn( ALOAD, 1 );
        Label continueLabel2 = new Label();
        methodVisitor.visitJumpInsn( IF_ACMPNE, continueLabel2 );
        DniInternal.writeConstantInt( methodVisitor, 1 );
        methodVisitor.visitInsn( IRETURN );
        methodVisitor.visitLabel( continueLabel2 );

        // if ( obj.getClass() != getClass() )
        //     return false;
        methodVisitor.visitVarInsn( ALOAD, 1 );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;" );
        methodVisitor.visitLdcInsn( this.parent.getImplType() );
        Label continueLabel3 = new Label();
        methodVisitor.visitJumpInsn( IF_ACMPEQ, continueLabel3 );
        DniInternal.writeConstantInt( methodVisitor, 0 );
        methodVisitor.visitInsn( IRETURN );
        methodVisitor.visitLabel( continueLabel3 );

        // <Type> other = (<Type>) obj;
        methodVisitor.visitVarInsn( ALOAD, 1 );
        methodVisitor.visitTypeInsn( CHECKCAST, getImplName() );
        methodVisitor.visitVarInsn( ASTORE, 2 );

        Label returnFalseLabel = new Label();

        for ( StructureField field : this.structureFieldFactory.getOrderedFields() )
        {
            // if ( other.get<field>() != get<field>() )
            //     return false;
            field.writeEqualsFragment( methodVisitor, returnFalseLabel, getImplName() );
        }

        // return true;
        DniInternal.writeConstantInt( methodVisitor, 1 );
        methodVisitor.visitInsn( IRETURN );

        // return false;
        methodVisitor.visitLabel( returnFalseLabel );
        DniInternal.writeConstantInt( methodVisitor, 0 );
        methodVisitor.visitInsn( IRETURN );

        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes the {@link Structure#hashCode()} method
     *
     * @param classWriter
     *            the class writer
     */
    private void writeHashCodeMethod( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC,
                                                               "hashCode",
                                                               "()I",
                                                               null,
                                                               null );
        methodVisitor.visitCode();

        // int hash = 0;
        methodVisitor.visitInsn( ICONST_0 );
        methodVisitor.visitVarInsn( ISTORE, 1 );

        for ( StructureField field : this.structureFieldFactory.getOrderedFields() )
        {
            // hash = hash * 31 + <hash-of-value>;
            methodVisitor.visitVarInsn( ILOAD, 1 );
            DniInternal.writeConstantInt( methodVisitor, 31 );
            methodVisitor.visitInsn( IMUL );

            field.writeHashCodeFragment( methodVisitor, getImplName() );

            methodVisitor.visitInsn( IXOR );
            methodVisitor.visitVarInsn( ISTORE, 1 );
        }

        // return hash;
        methodVisitor.visitVarInsn( ILOAD, 1 );
        methodVisitor.visitInsn( IRETURN );

        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes the {@link Structure#toString()} method
     *
     * @param classWriter
     *            the class writer
     */
    private void writeToDebugStringMethod( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC,
                                                               "toDebugString",
                                                               "()Ljava/lang/String;",
                                                               null,
                                                               null );
        methodVisitor.visitCode();

        // new StringBuilder()
        methodVisitor.visitTypeInsn( NEW, "java/lang/StringBuilder" );
        methodVisitor.visitInsn( DUP );
        methodVisitor.visitMethodInsn( INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V" );

        // .append( "<struct-name>[0x" )
        methodVisitor.visitLdcInsn( getType().getName() + "[0x" );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       "java/lang/StringBuilder",
                                       "append",
                                       "(Ljava/lang/String;)Ljava/lang/StringBuilder;" );

        // .append( Long.toHexString( this.pointer ) )
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitFieldInsn( GETFIELD, getImplName(), "pointer", "J" );
        methodVisitor.visitMethodInsn( INVOKESTATIC,
                                       "java/lang/Long",
                                       "toHexString",
                                       "(J)Ljava/lang/String;" );

        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       "java/lang/StringBuilder",
                                       "append",
                                       "(Ljava/lang/String;)Ljava/lang/StringBuilder;" );

        for ( StructureField field : this.structureFieldFactory.getOrderedFields() )
        {
            // .append( ", <field-name>=" )
            methodVisitor.visitLdcInsn( ", " + field.getName() + "=" );
            methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                           "java/lang/StringBuilder",
                                           "append",
                                           "(Ljava/lang/String;)Ljava/lang/StringBuilder;" );

            // .append( get<field>() )
            field.writeToStringFragment( methodVisitor, getImplName() );
        }

        // .append( "]" )
        methodVisitor.visitLdcInsn( "]" );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       "java/lang/StringBuilder",
                                       "append",
                                       "(Ljava/lang/String;)Ljava/lang/StringBuilder;" );

        // .toString()
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       "java/lang/StringBuilder",
                                       "toString",
                                       "()Ljava/lang/String;" );

        // return
        methodVisitor.visitInsn( ARETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes the {@link Structure#copy()} method
     *
     * @param classWriter
     *            the class writer
     */
    private void writeCopyMethod( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC,
                                                               "copy",
                                                               "()L" + STRUCTURE_TYPE + ";",
                                                               null,
                                                               null );
        methodVisitor.visitCode();

        // new <type>()
        methodVisitor.visitTypeInsn( NEW, getImplName() );
        methodVisitor.visitInsn( DUP );
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitMethodInsn( INVOKESPECIAL, getImplName(), "<init>", "(L" + getImplName() + ";)V" );

        // return
        methodVisitor.visitInsn( ARETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes the {@link Structure#copyFrom(Structure)} method
     *
     * @param classWriter
     *            the class writer
     */
    private void writeCopyFromMethod( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC,
                                                               "copyFrom",
                                                               "(L" + STRUCTURE_TYPE + ";)V",
                                                               null,
                                                               null );
        methodVisitor.visitCode();

        methodVisitor.visitVarInsn( ALOAD, 1 );
        methodVisitor.visitTypeInsn( CHECKCAST, getImplName() );
        methodVisitor.visitFieldInsn( GETFIELD, getImplName(), "pointer", "J" );
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitFieldInsn( GETFIELD, getImplName(), "pointer", "J" );
        DniInternal.writeConstantInt( methodVisitor, this.parent.getSize() );
        methodVisitor.visitInsn( I2L );
        methodVisitor.visitMethodInsn( INVOKESTATIC,
                                       MemoryAccess.UNSAFE_ACCESS_TYPE,
                                       "copyMemory",
                                       "(JJJ)V" );

        // return
        methodVisitor.visitInsn( RETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    private final class FieldComparator implements Comparator< StructureField >
    {
        @SuppressFBWarnings( value = "SC_SUSPICIOUS_COMPARATOR_RETURN_VALUES",
            justification = "If the values are the same, we throw" )
        @Override
        public int compare( final StructureField o1, final StructureField o2 )
        {
            if ( o1.getIndex() < o2.getIndex() )
            {
                return -1;
            }
            else if ( o1.getIndex() > o2.getIndex() )
            {
                return 1;
            }

            throw new IllegalArgumentException( "Duplicate index on fields: " + o1.getName() + " and " + o2.getName()
                                                + ": " + getType().getName() );
        }
    }

}
